#!/bin/bash
#
#  vboxtool: Utility to retrieve status and control VirtualBox sessions
#
#  Usage: Type 'vboxtool help' for more information
#
#  Copyright (C) 2008 Mark Baaijens <mark.baaijens@gmail.com>
#
#  This file is part of VBoxTool.
#
#  VBoxTool is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  VBoxTool is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

version()
{
  echo "VBoxTool version $version"
  echo "Copyright 2008 Mark Baaijens"
  echo "License GNU GPL version 3 or later"
  echo "Updated by Garrett McGrath - Princeton Neuroscience Institute, to integrate correction patches submitted to original sourceforge issue list"
}

usage()
{
  echo "Usage: vboxtool show|showrun|showconfig|start|autostart|save|stop|backup|version|help [session]"
  echo "Show info about VirtualBox sessions or control those sessions."
  echo "Type 'vboxtool help' for more information."    
}

help()
{
  echo "Usage: vboxtool OPTION [session]"
  echo "Show info about VirtualBox sessions or control those sessions."
  echo ""
  echo "Options:"
  echo "  show              Show status of all sessions."
  echo "  showrun           Only show status of running sessions."
  echo "  showconfig        Show configuration."
  echo "  start [session]   Start all saved sessions or only the given session."
  echo "                    When no session name is given, all saved sessions will be"
  echo "                    started; powered off and aborted sessions are left alone."  
  echo "  autostart         Starts all sessions in a predefined configuration file."
  echo "  save [session]    Save all running sessions or only the given session."
  echo "  stop [session]    Stop all running sessions or only the given session."
  echo "  backup [session]  Backup all running sessions or only the given session."
  echo "  --version|version Version info."
  echo "  --help|help       This help."
  echo ""
  echo "*Configuration. vboxtool depends on two config files, located in /etc/vboxtool."
  echo ""
  echo "Configuration file $machines_conf:"
  echo "- Each line in this file is a separate machine." 
  echo "- Structure of each line: <session name>,<vrde port>,<host port>-<guest port>|..."
  echo "- Do not use spaces before and after the ',' delimiter."  
  echo "- Lines can be commented out by '#'"
  echo ""  
  echo "Example for $machines_conf:"
  echo "Ubuntu Desktop,3391"
  echo "Ubuntu JeOS,3392,2022-22|80-80"
  echo ""
  echo "Example for $vboxtool_conf"
  echo "vbox_user='user'"
  echo "backup_folder=/home/user/vboxbackup"
  echo ""
  echo "*Autostart. Sessions can be started in a controlled way from the command line,"
  echo "only the echo sessions in $machines_conf will be started. As a bonus,"
  echo "the VRDP port and port forwarding can be set at startup time. These"
  echo "options are controlled by $machines_conf. The given ports"
  echo "are set statically to the session, prior to starting. When VRDP port has to be "
  echo "changed, state is discarded when session is in savestate."
  echo ""  
  echo "*Start at boot, save on halt. VBoxTool is capable for autostart sessions at"
  echo "boot time and autosave sessions when host is stopped. This depends on "
  echo "/etc/vboxtool/vboxtool.conf. In here, the variable vbox_user must be filled:"
  echo "vbox_user='<user name>'" 
  echo "Note the quotes. Fill for <user name> the name of the user under which"
  echo "sessions are installed/running."  
  echo ""  
  echo "When vboxtool.conf is not present, no session will start at boot, nor will"
  echo "auto save on host down take place. When vboxtool.conf is present, all sessions"
  echo "in machines.conf will be started because actually, a 'vboxtool autostart'"
  echo "command is issued. Saving sessions when host goes down does not depend on"
  echo "machines.conf: all running sessions will be saved by a 'vboxtool save' command."
  echo ""
  echo "*Stopping sessions. Saving sessions is preferred above stopping: this"
  echo "is faster when restoring and safer because session can appear to be cold booted."
  echo ""
  echo "*The backup command copies all session files to a safe location. This includes"
  echo "the configuration file(s), main VDI file and all snapshots. Running sessions"
  echo "are saved and started after backup has completed. The default backup folder is"
  echo "relative to the vbox folder: <vbox_folder>/.backup. Underneith, subfolders VDI and "
  echo "Machines are created."
  echo "A different folder can be used, by defining this in $vboxtool_conf:"
  echo "backup_folder=/home/user/vboxbackup"
  echo ""
  echo "*Logging. All commands will be logged to $log_file"
  echo ""    
  echo "See http://vboxtool.sourceforge.net for more details."  
}

log () { 
  # Log to console and a predefined log file.
  echo $1
  log2file "$1"
}

log2file () { 
  # Log to a predefined log file.
  echo "$(date +%Y-%m-%d) $(date +%H:%M:%S) $1" 1>> "$log_file"
}

showconfig()
{
  echo $vboxtool_conf
  cat $vboxtool_conf | while read conf_line
  do
    echo ' ' $conf_line
  done

  echo $machines_conf
  cat $machines_conf | while read conf_line
  do
    echo ' ' $conf_line
  done
}

loop()
{
  # Read commandline parameter(s)
  option=$1
  option_session_name=$2

  # Several state constants
  state_running='running'
  state_saved='saved'
  state_powered_off='powered-off'
  state_aborted='aborted'
  state_paused='paused'
  state_unknown='unknown'
      
  #
  # Iterate over all registered vm's
  #

  # Up to version 2.2.0, output of 'VBoxManage list vms | grep UUID:' looks like this:  
  # "UUID:<12 spaces><uuid>"
  # It is very much verbose, so lines have to be grepped.
  # For iterating over UUID's, this is the initial command:
  # VBoxManage list vms | grep UUID: | awk 'BEGIN{FS="UUID:            "}{print $2}'
  #
  # From version 2.2.0 and higher, output of 'VBoxManage list vms' looks like this:
  # "<session name>" {<uuid>} 
  # It's very compact, but nonetheless different from former versions, thus, not interchangeble.
  #
  # Fortunately, post 2.2.0 VBoxManage has an extra command option, --long. When applying that option, 
  # output is the same as pre 2.2.0 version of VBoxManage. However, simply applying that option on 
  # a pre-2.2.0 version, results in 'invalid command'. So first we have to distinguish if the --long 
  # option is needed. This can be done by retrieving version info of VBoxManage, but as this is error
  # prone (same math calc has to be done), I decided to evaluate the output of VBoxManage.

  # Detect a VBoxManage version post 2.2.0
  force_long_format=""
  long_format_test=$($vbox_command list vms | grep "UUID:")
  if ([ ! -n "$long_format_test" ]) 
  then
    force_long_format="--long"
  fi

  for uuid in $($vbox_command list vms $force_long_format | grep "UUID:            " | awk 'BEGIN{FS="UUID:            "}{print $2}')
  do
    #
    # Extract info from specific vm-session
    #

    # Beware: output from VBoxManage should be something like this
    # "Name:<12 spaces><uuid>"
    # "State:<11 spaces><uuid>"
    #name=$($vbox_command showvminfo $uuid      | grep "Name:"  | awk 'BEGIN{FS="Name:            "}{print $2}')
    name=$($vbox_command showvminfo $uuid      | sed -e'/^USB Device Filters:/,$ d' | grep "Name:"  | awk 'BEGIN{FS="Name:            "}{print $2}')
    state_raw=$($vbox_command showvminfo $uuid | grep "State:" | awk 'BEGIN{FS="State:           "}{print $2}')
    #vrdp_port=`$vbox_command showvminfo $uuid | grep VRDE: | awk '{ print $6}'`
    #vrdp_port=${vrdp_port/,/}  # Remove trailing comma
    vrde_port=`$vbox_command showvminfo $uuid | grep -P '^VRDE:\s+enabled\s+\(Address\s+\d+\.\d+\.\d+\.\d+,\s+Ports?\s+\d+,' | sed -e's/^.*, Ports* \([0-9][0-9]*\),.*$/\1/'`

    # Extract exact state from string state_raw
    # Beware: output from VBoxManage should be exactly as the given strings, i.e. 'running', 'saved', etc.
    echo "$state_raw" | grep -q "running"
    if [ $? -eq 0 ]
    then
      state=$state_running
    else
      echo "$state_raw" | grep -q "saved"
      if [ $? -eq 0 ]
      then
        state=$state_saved
      else
        echo "$state_raw" | grep -q "powered off"
        if [ $? -eq 0 ]
        then
          state=$state_powered_off
        else
          echo "$state_raw" | grep -q "aborted"
          if [ $? -eq 0 ]
          then
            state=$state_aborted
          else
            echo "$state_raw" | grep -q "paused"
            if [ $? -eq 0 ]
            then
              state=$state_paused
            else
              state=$state_unknown
            fi
          fi
        fi
      fi
    fi

    # Check for option-parameter
    case "$option" in
    save) # Save running sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then
        if [ "$state" == "$state_running" ]
        then
          log "Saving \"$name\""
          $vbox_command controlvm $uuid savestate
          log2file "Session \"$name\" saved"
        fi
      fi
      ;;
    backup) # Backup sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then

        # Save the session to provide a stabile snapshot
        if [ "$state" == "$state_running" ]
        then
          log "Pauzing \"$name\""
          $vbox_command controlvm $uuid pause
          log2file "Session \"$name\" paused"

          # Apparantly, saving a session is asynchronous, i.e. the session is not (entirely) 
          # saved even if the command line has returned. Starting the same session immediately
          # results in an error, stating the session is already running.
          sleep 1
        fi   

        # Files are copied on a individual basis to backup only those files which are essential and
        # to provide a clean backup. Log files under $machine_folder/$name/Logs are left out.
        mkdir -p "$backup_folder/Machines/$name/Snaphots"

        # Copy the session config file
        log2file "Copy $machine_folder/$name/$name.xml to "$backup_folder/Machines/$name""
        rsync -va "$machine_folder/$name/$name.xml" "$backup_folder/Machines/$name"

        # Copy any snapshots, i.e. delta's (save state = .vdi) + snapshots (= .sav)
        log2file "Copy $machine_folder/$name/Snapshots/ to "$backup_folder/Machines/$name/Snaphots""
        rsync -va --progress --delete "$machine_folder/$name/Snapshots/" "$backup_folder/Machines/$name/Snaphots"

        # Extract the VDI file name (main session file) of the specified session and copy it
        vdifile=$($vbox_command showvminfo "$name" | grep "Primary master" | cut -d":" -f2 | cut -d "(" -f1 | sed -e 's/^[ \t]*//' | sed 's/[ \t]*$//')
        log2file "Copy $vdifile to "$backup_folder/VDI""
        rsync -va --progress "$vdifile" "$backup_folder/VDI"

        # Restart session, only if is it was running before backing up
        if [ "$state" == "$state_running" ]
        then
          log "Resuming \"$name\""
          $vbox_command controlvm $uuid resume
          log2file "Session \"$name\" resumed"
        fi
      fi
      ;;
    stop) # Stop running sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then
        if [ "$state" == "$state_running" ]
        then
          log "Stopping \"$name\""
          # No reset, stopping is done by the operationg system within the session
          $vbox_command controlvm $uuid poweroff
          log2file "Session \"$name\" stopped"
        fi
      fi
      ;;
    start) # Start saved sessions
      # Sessions are started under the following conditions:
      # - when no session name is given, all saved sessions will be started
      # - (or) when a session name is given, only that specific session will be started
      start_session=0
      if ([ ! -n "$option_session_name" ] && [ "$state" == "$state_saved" ]) 
      then
        start_session=1
      else
        if ([ -n "$option_session_name" ] && [ "$name" == "$option_session_name" ])      
        then
          start_session=1        
        fi
      fi
      
      if [ "$start_session" == "1" ]
      then
        # In any case, the session to start must not be running already
        if [ "$state" != "$state_running" ]
        then   
          log "Starting \"$name\" (vrde=$vrde_port)"
          $vbox_command startvm $uuid --type headless #sampoedit, was -type vrdp
          log2file "Session \"$name\" started"
        fi
      fi
      ;;
    autostart) # Start sessions named in config file
      # Check existence of config file
      if [ -e "$machines_conf" ]
      then
        # Check if session is named in machines.conf. Watch the extra comma after name; 
        # this is to ensure the whole name is searched and found and not a substring.
        # This also requires the config file to be formatted like this: 
        # <session name>,<vrde port>
        conf_line=`cat $machines_conf | grep "$name,"`

        # Only start session when it is found, and not commented out by '#'                 
        if [ -n "$conf_line" ] && [ "${conf_line:0:1}" != "#" ]     
        then       
          # The session to start must not be running already
          if [ "$state" != "$state_running" ]
          then          
            # Extract VRDE port from machines.conf
            vrde_port_config=`echo $conf_line | awk 'BEGIN{FS=","}{print $2}'`

            # Check if configured port equals actual port
            if [ "$vrde_port_config" != "$vrde_port" ] 
            then 
              # Changing of the VRDE port can only take place on a powered-off session
              if [ "$state" == "$state_saved" ] 
              then
                log "Discarding state of \"$name\""
                $vbox_command discardstate $uuid
              fi

              log "Applying VRDE port $vrde_port_config to \"$name\""
              $vbox_command modifyvm $uuid --vrdeport $vrde_port_config
            fi

            #
            # Port forwarding
            #
            
            # Remove all port forwarding pairs containing 'vboxtool'; these are considered
            # 'property' of VBoxTool. Hence, they may be deleted at will (by VBoxTool).
            # By using such a strategy, we do not have to check if and how a particular 
            # port pair is defined; it's a kind of 'brute force' but it's very simple and
            # bullet proof to implement (KISS principle). This strategy also ensures that 
            # settngs are always removed, so that so setting becomes orphaned.
            for data_key in $($vbox_command getextradata $uuid enumerate | grep "VBoxInternal/Devices" | grep vboxtool | awk 'BEGIN{FS=","}{print $1}' | awk 'BEGIN{FS=": "}{print $2}')
            do
              # Variable data_key consist op the whole specifier, so inclusive trailing 'Protocol',
              # 'HostPort' or 'GuestPort'.
              $vbox_command setextradata $uuid $data_key
            done

            # Extract portforwarding definition from machines.conf
            # This string has the following syntax: <host port>-<guest port>|... 
            # For example: 2022-22|80-80
            port_forward_config=`echo $conf_line | awk 'BEGIN{FS=","}{print $3}'`

            # Is port forwarding defined?
            if [ -n "$port_forward_config" ] 
            then  
              # Iterate over all port-pairs defined in port_forward_config, separated by '|'
              port_forward_list=(`echo $port_forward_config | tr '|' ' '`)
              
              for port_pair in ${port_forward_list[@]}
              do 
                # Because port forwarding configuration can be made to the session, 
                # even when it is running (!) or when it is in save-state, there's no need 
                # to check if the session is in save-state (unlike configuring the VRDE port).
                         
                # Apply port forwarding settings
                log "Apply port forwarding $port_pair to \"$name\""
                
                # Variable data_id is only a party specifier, so without trailing 'Protocol',
                # 'HostPort' or 'GuestPort'.              
                data_id="VBoxInternal/Devices/pcnet/0/LUN#0/Config/vboxtool-tcp-$port_pair"
                $vbox_command setextradata $uuid $data_id/Protocol TCP
                $vbox_command setextradata $uuid $data_id/HostPort `echo $port_pair | awk 'BEGIN{FS="-"}{print $1}'`
                $vbox_command setextradata $uuid $data_id/GuestPort `echo $port_pair | awk 'BEGIN{FS="-"}{print $2}'`
              done
            fi
            
            log "Starting \"$name\" (vrde=$vrde_port_config)"
            $vbox_command startvm $uuid --type headless #sampoedit, was: -type vrdp
            log2file "Session \"$name\" started"
          fi        
        fi
      fi
      ;;      
    *) # Remaining parameters
      if [ "$state" == "$state_running" ]
      then
        #
        # Retrieve some runtime info for a running session
        #

        # Retrieve the pid of the vbox-session throuh 'ps'; note that only pid is extracted, not 
        # cpu or other info. These are drawn from the 'top' command because especially cpu from 'ps'
        # is not what is expected: it's an average cpu-load since the process started and not 
        # the actual cpu-load. 
        pid=$(ps -ef | grep "$name" | grep -v grep | awk '{ print $2 }')

        # The 'top' command delivers the actual cpu-load and memory consumed
        top=$(top -b -n1 -p $pid | grep $pid)
        cpu=`echo $top | awk '{ print $9}'`
        mem=`echo $top | awk '{ print $5}'`

        # Show some output
        echo "$name: state=$state vrde=$vrde_port cpu=$cpu% mem=$mem"
      else # Session is not running

        # Only show info when no option is given or the option is 'showrun'
        if [ -z "$option" ] || [ $option != "showrun" ] 
        then
          # Show some output
          echo "$name: state=$state vrde=$vrde_port"
        fi
      fi
      ;;
    esac
  done
}

#
# Retrieve vbox executable name
#

# The OSE-version uses a all lower case name, i.e. 'vboxmanage' so we
# have to find out which executable is available.
if [ -n $(whereis VBoxManage | awk 'BEGIN{FS=" "}{print $2}') ]
then
  vbox_command='VBoxManage -nologo'
else
  if [ -n $(whereis vboxmanage | awk 'BEGIN{FS=" "}{print $2}') ]
  then
    vbox_command='vboxmanage -nologo'
  else
    log "Either 'VBoxManage' or 'vboxmanage' is not available, exiting."
    exit 1
  fi
fi

# Some constants
version='0.5-pni branch'
machines_conf='/etc/vboxtool/machines.conf'
vboxtool_conf='/etc/vboxtool/vboxtool.conf'
vbox_folder="$HOME/.VirtualBox"   
log_file="$vbox_folder/vboxtool.log"

# Retrieve settings from config file, just by executing the config file.
# Config file $config_file should look like this:
# backup_folder="$vbox_folder/.backup"
if [ -f $vboxtool_conf ]
then
  . $vboxtool_conf
fi

# If no backup folder defined, use default
if [ ! -n "$backup_folder" ]
then
  backup_folder="$vbox_folder/.backup"
fi

#
# Check for a commandline option
#
case "$1" in
start)
  log2file "Started command: $1 $2"
  loop start "$2"
  log2file "Finished command: $1 $2"
  ;;
save)
  log2file "Started command: $1 $2"
  loop save "$2"
  log2file "Finished command: $1 $2"
  ;;
autostart)
  log2file "Started command: $1"

  # Check if config file exists
  if [ ! -e "$machines_conf" ]
  then
    log "Configuration file $machines_conf not found"
  fi
  loop autostart
  log2file "Finished command: $1"
  ;;  
stop)
  log2file "Started command: $1 $2"
  loop stop "$2"
  log2file "Finished command: $1 $2"
  ;;
backup)
  log2file "Started command: $1 $2"

  # Dynamic folders
  vdi_folder=$($vbox_command list systemproperties | grep "Default VDI" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')

  # Output stating VDI location is changed (somewhere in 2.x?) to "Default hard disk folder"; to provide 
  # backwards compatability, we should check that also
  if [ ! -n "$vdi_folder" ]
  then
    vdi_folder=$($vbox_command list systemproperties | grep "Default hard disk" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')
  fi

  machine_folder=$($vbox_command list systemproperties | grep "Default machine" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')

  log "Default hard disk folder: $vdi_folder"
  log "Default machine folder: $machine_folder"

  # Backup folder is fixed and relative to the vbox folder
  log "Backup folder: $backup_folder"

  # Create a backup folder and subfolders
  mkdir -p $backup_folder
  mkdir -p $backup_folder/Machines
  mkdir -p $backup_folder/VDI

  # Copy the vbox config file
  rsync -va "$vbox_folder/VirtualBox.xml" "$backup_folder"

  loop backup "$2"
  log2file "Finished command: $1 $2"
  ;;
show)
  loop show
  ;;
showrun)
  loop showrun
  ;;
showconfig)
  showconfig
  ;;
help)
  help
  ;;
--help)
  help  
  ;;
version)
  version
  ;;
--version)
  version
  ;;
*)
  usage
esac

exit 0

